Skip to content

Phase 2 endorsement response flow + accumulated staging work#61

Open
hb-agent wants to merge 836 commits into
mainfrom
staging
Open

Phase 2 endorsement response flow + accumulated staging work#61
hb-agent wants to merge 836 commits into
mainfrom
staging

Conversation

@hb-agent
Copy link
Copy Markdown
Collaborator

Headline feature: Phase 2 endorsement response flow

The recipient now has an explicit lever to hide unwanted endorsements via app.certified.badge.response. Closes the structural gap left by Phase 1: anyone could endorse anyone, but the recipient had no curation surface. Default-show stays the default (low friction); an opt-out (Hide) writes a rejected response on the recipient's own PDS and filters the award out of profile read paths.

What ships

  • Show / Hide buttons on /notifications rows where the underlying record is a badge.award. Loud, inline.
  • Quiet kebab menu on own-profile "Received" rows and on /endorsements Received tab. State indicator inside the menu ("Showing on your profile" / "Hidden from your profile" / etc.) plus actions for the current state.
  • Reset to default sweeps every response record on the award, returning to un-responded state.
  • Nav-badge pending counter on the Endorsements nav item (desktop rail + mobile sidebar) — the discovery cue that closes the default-show gap until the notifications backend learns about badge.award.
  • Undo toast after Hide (6 s, aria-live="polite", single-click revert).
  • Focus management after Hide moves to the next row's kebab (WAI-ARIA AC#7).
  • Roving tabindex inside the kebab menu (Arrow keys, Home/End, Esc).
  • Owner-only response state — non-owner viewers never receive per-row response data past the hook boundary. The visible-filter operates on the data without exposing it.
  • Mobile touch targets ≥44px on both controls.

Architecture decisions

Captured in writing per deep-flow §"Decisions belong in writing":

  • docs/badge-response-flow/plan.md — alternatives considered for default visibility (opt-out chosen), action-surface placement (loud notifications + quiet kebab), and the write strategy (append-only with rkey lexicographic tie-break for createdAt collisions).
  • docs/badge-response-flow/review-round-1.md — three parallel reviewer agents on the plan, each finding consolidated as accept/reject with rationale. Items folded back into the plan in place.
  • docs/badge-response-flow/review-round-1-impl.md — three parallel reviewer agents on the implementation, six critical items addressed in a follow-up commit (cross-hook staleness via useSyncExternalStore, focus + undo toast, roving tabindex, aria-label disambiguation, mobile touch targets, owner-only state indicator).

Accumulated non-feature work

Same PR, separate set of commits:

  • docs(agents) — adds the deep-flow process spec to AGENTS.md §26.
  • feat(endorsements) — Phase 1 migration onto app.certified.badge.{definition,award} from the legacy app.certified.temp.graph.endorsement lexicon (each user owns their endorsement badge).
  • fix(cache), fix(profile), fix(edit-profile), fix(left-rail), fix(news), fix(about), fix(legal), fix(apps), feat(news), refactor(groups) — accumulated polish since main was last cut. Per-commit messages describe scope.

Breaking changes

None at the contract level. The endorsement lexicon migration (Phase 1) deprecates app.certified.temp.graph.endorsement for new writes but the proxy allowlist still accepts the legacy collection so the feed-side "trusted evaluator" filter keeps working. A separate follow-up will migrate the feed filter onto badges.

Out of scope (filed as follow-ups)

  • badge.award rate-limiting on the xrpc proxy — abuse-mitigation, deferred per plan §"Out of scope" D1. Track as a separate security PR.
  • Notification-service awareness of badge.award — backend extension that lets app.certified.badge.award firehose events emit "endorsement" notifications. Today only legacy temp lexicon notifications surface. The nav-badge counter mitigates the discovery gap; the proper fix lives in the notifications backend (separate issue against the indexer team).
  • Indexer-side subject filter + nullable badge joinhb-agent/magic-indexer#65. When that lands, the PDS fan-out scan in useReceivedEndorsements collapses to a single GraphQL query.
  • Magic-indexer testdata lexicon drifthb-agent/magic-indexer#67 (PR landed for app/certified/*) and hb-agent/magic-indexer#68 (heads-up for org/hypercerts/*).

Verification

  • npx tsc --noEmit — 0 errors
  • npx eslint src/ — 0 errors, 28 warnings (baseline 27; +1 from a latest-ref-in-useEffect pattern in use-profile-responses; within plan AC#10 ±2 budget)
  • npx next build — green
  • Reviewer: manual smoke test of the response flow:
    • Sign in as account A; endorse account B from /endorsements "+ New"
    • Sign in as account B; open /endorsements Received tab — see A's endorsement
    • Click kebab → Hide from profile. Row disappears immediately. Toast appears with Undo.
    • Click Undo within 6 s. Row reappears.
    • Click kebab → Hide. Wait > 6 s. Toast disappears.
    • Re-open kebab → Reset to default. Row stays visible; vestigial responses cleared.
    • On account A's session, visit B's profile. Endorsement renders if visible to B; gone if B hid it.
    • On an anonymous tab, visit B's profile. Endorsement visibility matches B's curation.
    • Open the desktop nav — Endorsements item shows a small chip when there are pending awards.
    • Keyboard: Tab to kebab, Enter to open, ArrowDown to move, Esc to close (focus restored to trigger).

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
certified-app Ready Ready Preview, Comment Jun 5, 2026 4:46am
certified-app (staging) Ready Ready Preview, Comment Jun 5, 2026 4:46am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Important

Review skipped

Too many files!

This PR contains 187 files, which is 37 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 271ad903-8636-420c-bafa-95291ad89b31

📥 Commits

Reviewing files that changed from the base of the PR and between 0728b53 and becc274.

⛔ Files ignored due to path filters (113)
  • docs/design-audit/divergence/01-1-buttons_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/01-1-buttons_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/02-2-inputs_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/02-2-inputs_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/03-3-cards_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/03-3-cards_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/04-4-border-radius-drift_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/04-4-border-radius-drift_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/05-5-modals_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/05-5-modals_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/06-6-badges_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/06-6-badges_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/07-7-elevation_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/07-7-elevation_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/08-8-typography_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/08-8-typography_light.png is excluded by !**/*.png
  • docs/design-audit/divergence/_full_dark.png is excluded by !**/*.png
  • docs/design-audit/divergence/_full_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/01-root_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/01-root_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/01-root_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/01-root_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/02-landing_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/02-landing_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/02-landing_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/02-landing_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/03-home_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/03-home_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/03-home_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/03-home_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/04-explore_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/04-explore_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/04-explore_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/04-explore_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/05-search_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/05-search_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/05-search_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/05-search_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/06-notifications_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/06-notifications_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/06-notifications_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/06-notifications_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/07-profile-index_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/07-profile-index_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/07-profile-index_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/07-profile-index_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/08-settings_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/08-settings_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/08-settings_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/08-settings_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/09-edit-profile_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/09-edit-profile_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/09-edit-profile_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/09-edit-profile_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/10-groups_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/10-groups_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/10-groups_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/10-groups_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/11-groups-create_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/11-groups-create_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/11-groups-create_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/11-groups-create_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/12-create_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/12-create_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/12-create_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/12-create_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/13-project-new_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/13-project-new_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/13-project-new_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/13-project-new_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/14-apps_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/14-apps_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/14-apps_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/14-apps_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/15-about_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/15-about_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/15-about_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/15-about_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/16-privacy_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/16-privacy_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/16-privacy_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/16-privacy_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/17-terms_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/17-terms_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/17-terms_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/17-terms_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/18-dsa_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/18-dsa_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/18-dsa_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/18-dsa_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/19-imprint_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/19-imprint_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/19-imprint_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/19-imprint_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/20-endorsements_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/20-endorsements_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/20-endorsements_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/20-endorsements_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/21-workspace_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/21-workspace_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/21-workspace_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/21-workspace_mobile_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/22-404_desktop_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/22-404_desktop_light.png is excluded by !**/*.png
  • docs/design-audit/screenshots/22-404_mobile_dark.png is excluded by !**/*.png
  • docs/design-audit/screenshots/22-404_mobile_light.png is excluded by !**/*.png
  • docs/design-consolidation/after/apps_dark.png is excluded by !**/*.png
  • docs/design-consolidation/after/apps_light.png is excluded by !**/*.png
  • docs/design-consolidation/after/landing_dark.png is excluded by !**/*.png
  • docs/design-consolidation/after/landing_light.png is excluded by !**/*.png
  • docs/design-consolidation/after/privacy_dark.png is excluded by !**/*.png
  • docs/design-consolidation/after/privacy_light.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (187)
  • .agents/skills/impeccable/SKILL.md
  • .agents/skills/impeccable/reference/adapt.md
  • .agents/skills/impeccable/reference/animate.md
  • .agents/skills/impeccable/reference/audit.md
  • .agents/skills/impeccable/reference/bolder.md
  • .agents/skills/impeccable/reference/brand.md
  • .agents/skills/impeccable/reference/clarify.md
  • .agents/skills/impeccable/reference/cognitive-load.md
  • .agents/skills/impeccable/reference/color-and-contrast.md
  • .agents/skills/impeccable/reference/colorize.md
  • .agents/skills/impeccable/reference/craft.md
  • .agents/skills/impeccable/reference/critique.md
  • .agents/skills/impeccable/reference/delight.md
  • .agents/skills/impeccable/reference/distill.md
  • .agents/skills/impeccable/reference/document.md
  • .agents/skills/impeccable/reference/extract.md
  • .agents/skills/impeccable/reference/harden.md
  • .agents/skills/impeccable/reference/heuristics-scoring.md
  • .agents/skills/impeccable/reference/interaction-design.md
  • .agents/skills/impeccable/reference/layout.md
  • .agents/skills/impeccable/reference/live.md
  • .agents/skills/impeccable/reference/motion-design.md
  • .agents/skills/impeccable/reference/onboard.md
  • .agents/skills/impeccable/reference/optimize.md
  • .agents/skills/impeccable/reference/overdrive.md
  • .agents/skills/impeccable/reference/personas.md
  • .agents/skills/impeccable/reference/polish.md
  • .agents/skills/impeccable/reference/product.md
  • .agents/skills/impeccable/reference/quieter.md
  • .agents/skills/impeccable/reference/responsive-design.md
  • .agents/skills/impeccable/reference/shape.md
  • .agents/skills/impeccable/reference/spatial-design.md
  • .agents/skills/impeccable/reference/teach.md
  • .agents/skills/impeccable/reference/typeset.md
  • .agents/skills/impeccable/reference/typography.md
  • .agents/skills/impeccable/reference/ux-writing.md
  • .agents/skills/impeccable/scripts/cleanup-deprecated.mjs
  • .agents/skills/impeccable/scripts/command-metadata.json
  • .agents/skills/impeccable/scripts/design-parser.mjs
  • .agents/skills/impeccable/scripts/detect-csp.mjs
  • .agents/skills/impeccable/scripts/impeccable-paths.mjs
  • .agents/skills/impeccable/scripts/is-generated.mjs
  • .agents/skills/impeccable/scripts/live-accept.mjs
  • .agents/skills/impeccable/scripts/live-browser-session.js
  • .agents/skills/impeccable/scripts/live-browser.js
  • .agents/skills/impeccable/scripts/live-complete.mjs
  • .agents/skills/impeccable/scripts/live-completion.mjs
  • .agents/skills/impeccable/scripts/live-inject.mjs
  • .agents/skills/impeccable/scripts/live-poll.mjs
  • .agents/skills/impeccable/scripts/live-resume.mjs
  • .agents/skills/impeccable/scripts/live-server.mjs
  • .agents/skills/impeccable/scripts/live-session-store.mjs
  • .agents/skills/impeccable/scripts/live-status.mjs
  • .agents/skills/impeccable/scripts/live-wrap.mjs
  • .agents/skills/impeccable/scripts/live.mjs
  • .agents/skills/impeccable/scripts/load-context.mjs
  • .agents/skills/impeccable/scripts/modern-screenshot.umd.js
  • .agents/skills/impeccable/scripts/pin.mjs
  • .env.local.example
  • .github/CODEOWNERS
  • .github/workflows/ci.yml
  • .gitignore
  • .impeccable/design.json
  • .nvmrc
  • AGENTS.md
  • AUDIT_REPORT.md
  • CHANGELOG.md
  • CHANGES.md
  • CLAUDE.md
  • DESIGN.md
  • PRODUCT.md
  • REVIEW.md
  • certified-design.pen
  • docs/auth-bsky-pds-fix/plan.md
  • docs/auth-bsky-pds-fix/review-round-1.md
  • docs/auth-bsky-pds-fix/review-round-2.md
  • docs/auth-bsky-pds-fix/review-round-3.md
  • docs/auth-bsky-pds-fix/tsc-baseline.txt
  • docs/badge-response-flow/plan.md
  • docs/badge-response-flow/review-round-1-impl.md
  • docs/badge-response-flow/review-round-1.md
  • docs/code-quality-audit/00-baseline.md
  • docs/code-quality-audit/01-inventory/components.md
  • docs/code-quality-audit/01-inventory/hooks.md
  • docs/code-quality-audit/01-inventory/lib-utilities.md
  • docs/code-quality-audit/01-inventory/routes.md
  • docs/code-quality-audit/02-findings/dead-code.md
  • docs/code-quality-audit/02-findings/naming-and-structure.md
  • docs/code-quality-audit/02-findings/quality-other.md
  • docs/code-quality-audit/02-findings/reuse.md
  • docs/code-quality-audit/02-findings/type-safety.md
  • docs/code-quality-audit/03-plan-critique.md
  • docs/code-quality-audit/03-plan-v1.md
  • docs/code-quality-audit/03-plan-v2.md
  • docs/code-quality-audit/04-track-logs/track-01-dead-code-sweep.md
  • docs/code-quality-audit/04-track-logs/track-02-formatShortDate-in-activity-detail.md
  • docs/code-quality-audit/04-track-logs/track-03-formatMonthYear.md
  • docs/code-quality-audit/04-track-logs/track-04-getInitials.md
  • docs/code-quality-audit/04-track-logs/track-05-handle-search-dead-isDid.md
  • docs/code-quality-audit/04-track-logs/track-06-useMounted.md
  • docs/code-quality-audit/04-track-logs/track-07-relocate-layout-breakpoints.md
  • docs/code-quality-audit/04-track-logs/track-08-redactSecrets-dedupe.md
  • docs/code-quality-audit/04-track-logs/track-09-rename-bsky-route-helper.md
  • docs/code-quality-audit/04-track-logs/track-10-extractRecordRef.md
  • docs/code-quality-audit/04-track-logs/track-12-asBlobRef.md
  • docs/code-quality-audit/04-track-logs/track-13-xrpcGetRecordPath.md
  • docs/code-quality-audit/04-track-logs/track-14-log-safe-test-coverage.md
  • docs/code-quality-audit/05-changelog.md
  • docs/code-quality-audit/06-deferred.md
  • docs/code-quality-audit/07-methodology.md
  • docs/code-quality-audit/round-2/00-delta-from-round-1.md
  • docs/code-quality-audit/round-2/01-inventory/inventory.md
  • docs/code-quality-audit/round-2/02-findings/findings.md
  • docs/code-quality-audit/round-2/03-plan-critique-1.md
  • docs/code-quality-audit/round-2/03-plan-critique-2.md
  • docs/code-quality-audit/round-2/03-plan-v1.md
  • docs/code-quality-audit/round-2/03-plan-v2.md
  • docs/code-quality-audit/round-2/03-plan-v3.md
  • docs/code-quality-audit/round-2/04-track-logs/track-summary.md
  • docs/code-quality-audit/round-2/05-changelog.md
  • docs/code-quality-audit/round-2/06-deferred.md
  • docs/code-quality-audit/round-2/07-methodology.md
  • docs/component-library/analysis.md
  • docs/component-library/decisions.html
  • docs/component-library/index.html
  • docs/current-state/feature-inventory.md
  • docs/current-state/review-round-1.md
  • docs/current-state/review-round-2.md
  • docs/design-audit/component-audit.md
  • docs/design-audit/divergence-sheet.html
  • docs/design-audit/screenshots/_manifest.json
  • docs/design-audit/visual-divergence.md
  • docs/design-consolidation/plan.md
  • docs/desktop-bottom-navbar-loading/discovery.md
  • docs/desktop-bottom-navbar-loading/plan.md
  • docs/desktop-bottom-navbar-loading/review-round-1.md
  • docs/desktop-bottom-navbar-loading/review-round-2.md
  • docs/desktop-layout/plan.md
  • docs/desktop-layout/review-round-1.md
  • docs/desktop-layout/review-round-2.md
  • docs/endorsements-badge-blue-comparison-v2.md
  • docs/endorsements-badge-blue-comparison.md
  • docs/endorsements-indexer-filter/plan.md
  • docs/follower-events-feed/discovery.md
  • docs/follower-events-feed/implementation.md
  • docs/follower-events-feed/plan-v2.md
  • docs/follower-events-feed/plan.md
  • docs/follower-events-feed/review-build.md
  • docs/follower-events-feed/review-decisions.md
  • docs/follower-events-feed/review-impl-correctness.md
  • docs/follower-events-feed/review-impl-integration.md
  • docs/follower-events-feed/review-impl-quality.md
  • docs/follower-events-feed/review-integration.md
  • docs/follower-events-feed/review-spec.md
  • docs/funding-receipts-comparison-v2.md
  • docs/funding-receipts-comparison.md
  • docs/group-membership-architecture.md
  • docs/groups-list-improvements/review-round-1.md
  • docs/groups-list-improvements/review-round-2.md
  • docs/issue-67/plan.md
  • docs/issue-67/review-round-1.md
  • docs/lists-as-collections/discovery.md
  • docs/lists-as-collections/plan.md
  • docs/lists-as-collections/review-round-1.md
  • docs/lists-as-collections/review-round-2.md
  • docs/overnight-2026-05-18/00-orientation.md
  • docs/overnight-2026-05-18/01-review-plan.md
  • docs/overnight-2026-05-18/02-findings-lens-6-perf-a11y.md
  • docs/overnight-2026-05-18/02-findings.md
  • docs/overnight-2026-05-18/03-implementation-plan.md
  • docs/overnight-2026-05-18/04-mini-review-1.md
  • docs/overnight-2026-05-18/04-mini-review-2.md
  • docs/overnight-2026-05-18/04-mini-review-3.md
  • docs/overnight-2026-05-18/05-final-review.md
  • docs/profile-rendering/plan.md
  • docs/quality-pass/plan.md
  • docs/quality-pass/review-round-1.md
  • docs/resolve-did-batch/plan.md
  • docs/resolve-did-batch/review-round-1.md
  • docs/switcher-mockups/option-1-desktop.html
  • docs/switcher-mockups/option-1-sticky-bar.html
  • docs/switcher-mockups/option-2-desktop.html
  • docs/switcher-mockups/option-2-hybrid.html
  • docs/switcher-mockups/option-3-github-model.html
  • docs/switcher-mockups/pattern-gallery.html
  • eslint.config.mjs
  • next.config.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@holkexyz holkexyz marked this pull request as ready for review May 13, 2026 15:13
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

holkexyz and others added 8 commits June 3, 2026 22:22
Closes the notifications slice of the org-identity model: a user who
owns/admins a group can see that group's notifications ('your group was
endorsed') alongside their own. Gated behind NOTIFICATIONS_AGGREGATION
(default OFF) because it needs the magic-indexer `recipients` change first
(see docs/org-identity/indexer-notifications-aggregation.md); with the flag
off the page + badge are byte-identical to before.

- config: NOTIFICATIONS_AGGREGATION_ENABLED (NEXT_PUBLIC_, read server +
  client).
- notifications client: fetchNotifications/fetchUnreadCount take an
  optional `recipients` set (sent only when non-empty) and parse the new
  per-notification `recipient` field.
- route: a `recipients` query variant selected ONLY when the flag is on +
  recipients are present and valid (DID-shaped, deduped, capped) — so an
  indexer without the arg never receives it.
- use-managed-notifications: aggregated feed across useManagedAuthors(),
  each row tagged via ownerTagForDid(recipient).
- notifications-context: the unread badge counts across managed recipients
  when the flag is on (NotificationsProvider sits under OrgProvider).
- notifications page: an identity focus filter (shared useIdentityFocus)
  and a 'via {group}' byline on group rows; group rows are read-only —
  the endorsement accept/reject control is suppressed so the viewer can't
  respond as their personal account to an award made to the group.
  Wrapped in Suspense for the useSearchParams static-prerender bailout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- managed fixtures: per-recipient notification notices (member group never
  a recipient) + an aggregated unread count.
- mock-fetch-provider: /api/notifications honours `recipients`, serving the
  managed notifications connection (with `recipient`) + summed unread count.
- preview harness: a 'notifications' surface (managed scenario) so the
  aggregated UI is browser-verifiable with NEXT_PUBLIC_NOTIFICATIONS_AGGREGATION=true.
- unit tests: recipients are omitted from the request body by default and
  included only when non-empty; the `recipient` field round-trips; the
  fixtures tag recipients correctly and exclude the member group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the switcher

Two majors from the review:

- Home sidebar misattribution: switching into a group dropped the 'via
  {group}' byline for ALL rows without filtering the list to that group, so
  personal + other-group records read as if they belonged to the focused
  group. Removed the activeOrg coupling entirely — Home always shows the
  full managed aggregate with provenance always visible (acting-as is
  read-scope and never strips what's yours); /managed is where the focus
  filter narrows to one owner. 'Show more' now points at /managed (the
  aggregate it previews).
- Edit eligibility was gated on the org switcher, so a record surfaced via
  read-aggregation couldn't be edited without first switching into its
  group — breaking the read->edit path. Eligibility now derives from the
  managed-author set (personal + owned/admin groups), and both edit pages
  wait on the managed-author load for non-personal records so a managed
  group record never flashes 'you can't edit'. The BFF still re-checks role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ViaByline was invisible to screen readers (all children aria-hidden, a
  bare aria-label on a roleless span isn't announced). The visible 'via
  {name}' text is now the accessible name; the role is appended sr-only.
- PostingAs menu now shows a reserved-width check on the current identity.
- Follow/Endorse pickers stored only the selected DID and derive the
  identity each render, so the picker shows the freshest resolved option
  instead of a stale postingOptions[0] snapshot.
- Notifications held both feed hooks until the org roles resolve, so a
  managing viewer no longer fires a throwaway personal fetch / flashes
  personal-only before aggregation engages.
- Focus filters scroll horizontally on narrow viewports (no overflow/wrap);
  the notifications dropdown is width-capped to match /managed.
- Profile bridge hover uses --color-accent-hover (theme-correct 'stronger'
  in both modes) instead of the lighter --color-accent.
- Dropped an orphan (.managed-row__via-label) and an unused
  (.notification-row__via) class.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extracted the notifications operation allowlist, query variants, recipient
  validation, and per-op variable normalization into ./operations (flag
  injected as a param) so the flag-gate is unit-testable without the full
  route. operations.test.ts asserts the flag-OFF path never emits recipients
  / never selects the aggregated variant, and the flag-ON path validates +
  dedups + caps recipients and picks the variant. parseRecipients now reuses
  isValidDid instead of a looser did:-prefix check.
- owner-tag.test.ts pins the tagging contract incl. the 'never label a
  stranger You' safety branch.
- fan-out.test.ts pins per-DID error isolation + AbortError re-throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion

feat: org-identity aggregation — surface group-owned work, per-action write identity, flag-gated notifications
holkexyz added 2 commits June 4, 2026 10:29
…r-action write identity, flag-gated notifications"
…ity-aggregation

Revert "feat: org-identity aggregation — surface group-owned work, per-action write identity, flag-gated notifications"
holkexyz and others added 5 commits June 4, 2026 09:56
Shared primitives for showing records owned by the groups a user
owns/admins on their own surfaces, tagged 'by {group}':

- lib/groups/managed.ts: ownedOrAdminGroups (member-role excluded) +
  managedAuthorDids (the 'My X' author set — viewer + owned/admin groups,
  or just the active group when acting-as), both pure + tested.
- use-managed-authors / use-managed-projects / use-managed-activities:
  aggregate fetchProjects/fetchIndexerActivities across the managed author
  set and owner-tag each record; the project/activity hooks take an
  `enabled` flag so a caller can switch aggregation off (foreign profiles)
  without breaking the rules of hooks.
- atproto/owner-tag.ts: ownerTagForDid/ownerTagForUri — never mislabels a
  stranger's record as 'You' (tested).
- ui/owner-byline.tsx: the compact 'by {group}' byline (avatar + name; role
  announced sr-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'My projects' / 'My activities' now read through the managed hooks, so
records owned by the groups the viewer owns/admins surface alongside their
personal ones, each tagged 'by {group}'. Personal records carry no byline.
The sidebar is personal-anchored (acting-as is read-scope and never changes
what's 'yours' here).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ions'

- 'My projects' / 'My activities' (the by-me filter) now query the managed
  author set (viewer + owned/admin groups), so group-owned records appear,
  attributed to the group by the row's existing author column.
- 'My organizations' filter fixed: it resolved groups by scanning the
  most-recently-indexed top-100 actors and filtering client-side, so any
  org outside that window silently dropped. It now resolves the viewer's
  group DIDs DIRECTLY via fetchNetworkActorsByDids — every org shows. The
  stale top-100 client-filter clause + its now-wrong comment are removed so
  the path can't be reintroduced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On the viewer's OWN personal profile only (isOwnProfile && !activeOrg) the
tabs aggregate records owned by the groups the viewer owns/admins; foreign
and group profiles are unchanged.

- Projects tab: aggregated boxes render a 'by {group}' byline under the
  title; the aggregated set can span pages, so the own-profile view
  paginates (Load more).
- Activities tab: useUserIndexerActivities gains an optional authoredAuthors
  set — the 'Created' bucket fans out across the managed authors via the
  multi-author op, so group-authored activities appear with the group shown
  as the card's author (the dids map carries the owning DID). 'Contributed'
  stays the viewer's personal contributions. A generation guard drops a
  stale page if the author set changes mid-loadMore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auth-mock preview harness gains a ?managed=1 scenario: the mock session
belongs to three groups (owner/admin/member) and the indexer serves the
records authored by the owner/admin ones, so the inline 'by {group}'
aggregation on the Home sidebar + the profile Projects/Activities tabs can
be browser-verified logged-out. Member-role exclusion is pinned at the
fixture level. Dev-only — the preview route notFound()s in production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigation:
- Top bar: add Home, Explore, and My profile icons around the existing
  Apps/Settings cluster (auth-gated where appropriate).
- Left rail: move Apps to directly after Explore.
- Create flows (/create, /project/new) now get the row-2 Back affordance.

Explore:
- List-row author bylines: zero the card-view bottom margin inside
  .cert-list-row__author-col so they sit vertically centered.
- "My organizations" filter now renders every group the viewer belongs
  to (synthesised per-row), not just those the indexer has a profile for.

Edit project form:
- EditBanner Save button rendered dark-on-dark: two equal-specificity
  arbitrary Tailwind text utilities resolved by stylesheet order. Force
  the primary label color with !text so it always wins.
- Constrain the editing banner to the article's reading column
  (.edit-banner--project) instead of the full --wide content width.

Permissions:
- Show the Edit button on an activity/project when the viewer is an
  owner/admin of the owning group but signed in as their individual
  user. Clicking opens a confirm modal explaining they'll edit as the
  group, then switches into it and opens the editor. Delete stays gated
  to direct owners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
holkexyz and others added 2 commits June 4, 2026 15:29
The /search people-search page is redundant with /explore. Remove the
route and repoint every "Explore" nav entry (left rail, mobile sidebar,
bottom nav) at /explore. Add a permanent /search -> /explore redirect so
old / indexed links don't 404, drop /search from robots disallow, and
delete the now-orphaned PeopleSearch component. The global search field
and /api/search-actors typeahead are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
holkexyz and others added 2 commits June 4, 2026 16:40
…n edge

The banner was capped to the 960px column but the article column has
16px side padding, so the hero/title sit in a 928px box and the banner
stuck out 16px each side. Cap the banner to the column width minus that
padding so its edges line up with the visible content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The home-feed row's date track was auto-sized, so a relative "2d ago"
and an absolute "2026-05-20" produced different column widths and the
dates didn't align across rows. Pin the date track to 84px (matching
the explore list) and right-align so every date sits in the same box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stack the icon + label vertically for Home, Explore, Apps, Profile
and Settings in the desktop top bar, LinkedIn-style. Drop the now-
redundant title tooltips; keep aria-labels for screen readers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants